================================================================================
TEPS REGISTRATION CORE — API REFERENCE
================================================================================

Contributors: Mikal Farley
Created:      April 2026
Last Updated: April 11, 2026

Base URL:     http://<server-ip>:8585 (default port, configurable)
Content-Type: application/json (all request and response bodies)


================================================================================
CONVENTIONS
================================================================================

  - All responses are JSON unless otherwise noted.
  - Error responses use: {"error": "<message>"}
  - Registration fields accept both snake_case and camelCase in request bodies.
    Responses always use snake_case (via CodingKeys).
  - Boolean fields are true/false in JSON, stored as INTEGER 0/1 in SQLite.
  - Timestamps are ISO 8601: "2026-04-11T14:30:00Z"
  - IDs are integers (Int64).
  - The server must have an active event for most endpoints to work.
    If none exists, endpoints return 404 "No active event".


================================================================================
1. STATUS & CONFIGURATION
================================================================================


--- GET /api/status ----------------------------------------------------------

  Server health check and event summary.

  Response 200:
  {
    "status": "running",
    "version": "1.0",
    "port": 8585,
    "event_name": "Holiday Photos 2026",
    "has_event": true,
    "total_registered": 45,
    "total_checked_in": 12,
    "total_complete": 28,
    "schedule_date": "2026-04-11",       // only if schedule loaded
    "total_slots": 120                    // only if schedule loaded
  }


--- GET /api/event -----------------------------------------------------------

  Active event details.

  Response 200:
  {
    "id": 1,
    "name": "Holiday Photos 2026",
    "date": "2026-04-11",
    "api_source_url": null,
    "synced_at": null
  }

  Response 404: No active event.


--- POST /api/events ---------------------------------------------------------

  Create a new event (becomes the active event).

  Request body:
  {
    "name": "Holiday Photos 2026",     // required
    "date": "2026-04-11",              // optional, defaults to today
    "api_source_url": "https://..."    // optional
  }

  Response 201: Event object (same shape as GET /api/event).


--- GET /api/config/fields ---------------------------------------------------

  Registration form field configuration. Drives the dynamic form on
  /registration and /hoststation.

  Response 200:
  {
    "fields": [
      {
        "field": "name",
        "label": "Name",
        "enabled": true,
        "required": false,
        "firstAndLast": true          // only on "name" field
      },
      {
        "field": "email",
        "label": "Email Address",
        "enabled": true,
        "required": true
      },
      ...
    ],
    "allowDuplicates": true,
    "allowMultiple": true,
    "sendTextMessage": false,
    "sendEmail": false,
    "requireEmailOrPhone": false,
    "allowRequiredOverride": true,
    "nameRequired": false,
    "phoneCountry": "US",
    "registerButtonText": "REGISTER",
    "cancelButtonText": "",
    "optIns": [                        // only if configured
      {
        "key": "marketing",
        "label": "I'd like to receive updates and special offers",
        "required": false
      }
    ],
    "customFields": [                  // only if configured
      {
        "field": "shirt_size",
        "label": "Shirt Size",
        "type": "select",
        "options": ["S", "M", "L", "XL"],
        "enabled": true,
        "required": false,
        "isCustom": true
      }
    ],
    "petTypeOptions": ["Cat", "Dog"],  // only if pet field configured
    "walkinCards": [                    // only if walk-in profile active
      {
        "id": "uuid",
        "title": "Contact Information",
        "subtitle": "Please provide your details",
        "fields": ["name", "email", "phone"],
        "buttonText": "Continue"
      }
    ]
  }


--- GET /api/config/statuses -------------------------------------------------

  Maps status strings to human-readable display names.

  Response 200:
  {
    "registered": "Waiting",
    "checked_in": "Checked In",
    "elf_processed": "Elf Done",
    "photographed": "Camera",
    "previewing": "Preview",
    "checkout": "Checkout",
    "complete": "Complete",
    "cancelled": "Cancelled",
    "no_show": "No Show"
  }


--- POST /api/validate/email -------------------------------------------------

  Server-authoritative email validation. Use this instead of client-side
  validation to ensure consistent rules.

  Request body:
  {
    "email": "user@example.com"
  }

  Response 200 (valid):
  {
    "valid": true
  }

  Response 200 (invalid):
  {
    "valid": false,
    "error": "Please enter a valid email address"
  }

  Rejects: numeric-only local parts, known disposable domains, trash patterns.


--- GET /api/schedule --------------------------------------------------------

  Today's time slots and reservations (from cloud sync or local schedule).

  Response 200:
  {
    "has_schedule": true,
    "schedule_date": "2026-04-11",
    "blocks": [
      {
        "block_id": 1,
        "service_id": 1,
        "service_name": "Photo Session",
        "start_time": "09:00:00",
        "end_time": "12:00:00",
        "label": "Photo Session",
        "block_state": "active"
      }
    ],
    "time_slots": [
      {
        "slot_id": 1,
        "service_id": 1,
        "slot_date": "2026-04-11",
        "start_time": "09:00:00",
        "end_time": "09:05:00",
        "capacity": 1,
        "booked_count": 1,
        "held_count": 0,
        "status": "available",
        "reservations": [
          {
            "confirmation_code": "RES-12345",
            "customer_first_name": "Jane",
            "customer_last_name": "Doe",
            "customer_email": "jane@example.com",
            "customer_phone": "5551234567",
            "party_size": 2,
            "status": "confirmed",
            "sync_id": "uuid-string"
          }
        ]
      }
    ]
  }

  Response 200 (no schedule):
  {
    "has_schedule": false,
    "schedule_date": "",
    "blocks": [],
    "time_slots": []
  }


================================================================================
2. REGISTRATION CRUD
================================================================================


--- GET /api/registrations ---------------------------------------------------

  List registrations for the active event. Supports filtering.

  Query parameters (all optional):
    ?status=checked_in        Filter by status
    ?station=camera           Filter by current station
    ?code=0411A3BF7K2P        Lookup by photo code (returns array of 0 or 1)
    ?date=2026-04-11          Filter by creation date

  Response 200: Array of Registration objects.
  [
    {
      "id": 1,
      "event_id": 1,
      "sync_id": "uuid",
      "source_type": 1,
      "code": "0411A3BF7K2P",
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane@example.com",
      "phone": "5551234567",
      "child_name": "Alice;Bob",
      "child_wishlist": "Doll;Train",
      "child_age": "5;7",
      "elf_name": null,
      "postal_code": "90210",
      "street_address": null,
      "date_of_birth": null,
      "age_range": null,
      "group_size": 3,
      "optin": false,
      "optins": "{\"marketing\": true}",
      "notes": null,
      "order_text": null,
      "promo_code": null,
      "custom_fields": null,
      "pet_type": null,
      "pet_breed": null,
      "status": "checked_in",
      "current_station": "host",
      "reservation_time": "09:30:00",
      "is_late": false,
      "fastpass": false,
      "fastpass_status": null,
      "email_uploaded": false,
      "userdata_uploaded": false,
      "text_uploaded": false,
      "data_declined": false,
      "created_at": "2026-04-11T09:25:00Z",
      "updated_at": "2026-04-11T09:30:00Z",
      "status_display": "Checked In",
      "structured_notes": null,
      "is_queue_active": null
    }
  ]


--- POST /api/registrations -------------------------------------------------

  Create a new registration.

  Request body:
  {
    "first_name": "Jane",              // or "firstName" or "name" (auto-split)
    "last_name": "Doe",                // or "lastName"
    "email": "jane@example.com",       // validated server-side
    "phone": "(555) 123-4567",         // auto-normalized to digits
    "child_name": "Alice;Bob",         // semicolon-delimited for multi-child
    "child_wishlist": "Doll;Train",
    "child_age": "5;7",
    "postal_code": "90210",
    "street_address": "123 Main St",
    "date_of_birth": "1990-01-15",
    "age_range": "25-34",
    "group_size": 3,
    "optin": true,
    "optins": {"marketing": true, "sms": false},  // object or JSON string
    "notes": "VIP customer",
    "order_text": "Premium package",
    "promo_code": "HOLIDAY20",
    "custom_fields": {"shirt_size": "M"},          // object or JSON string
    "pet_type": "Dog",
    "pet_breed": "Golden Retriever",
    "reservation_time": "09:30:00",
    "fastpass": false,
    "data_declined": false,
    "source_type": 1                   // 0=reservation, 1=walkin_local, 2=walkin_web
  }

  All fields except first_name are optional.

  Response 201: Full Registration object (includes generated code, sync_id).

  Response 400: Validation error.
  {
    "error": "Please enter a valid email address",
    "field": "email"
  }

  Response 409: Duplicate detected (when allowDuplicates is off).
  {
    "error": "A registration with this email already exists",
    "field": "email"
  }

  Notes:
    - Photo code is auto-generated from RegistrationConfig.photoCodeSegments.
    - Phone is stripped to digits only.
    - Email is validated against the same rules as POST /api/validate/email.
    - The "name" field is auto-split on first space into first_name/last_name.


--- GET /api/registrations/{id} ----------------------------------------------

  Get a single registration by ID.

  Response 200: Registration object.
  Response 404: Not found.


--- PUT /api/registrations/{id} ----------------------------------------------

  Update fields on a registration. Only include fields you want to change.

  Request body (partial update):
  {
    "first_name": "Janet",
    "elf_name": "Sparkle",
    "notes": "wishlist: Bicycle\nelf: Sparkle"
  }

  Accepts both snake_case and camelCase keys. Auto-maps:
    firstName -> first_name, childName -> child_name, etc.

  Phone is re-normalized. Custom fields and optins are re-serialized if
  provided as objects.

  Response 200: Updated Registration object.
  Response 404: Not found.


--- GET /api/registrations/code/{code} ---------------------------------------

  Lookup a registration by photo code. Returns the registration plus
  TEPS-REG-* response headers for inter-station data transfer.

  Response 200: Registration object (full customer data is in the JSON body).
  Response headers (non-PII operational only — see section 9):
    TEPS-REG-CODE: 0411A3BF7K2P
    TEPS-REG-FASTPASS: FALSE
    TEPS-REG-FASTPASSSTATUS:
    TEPS-REG-TOTAL: 49.99           // if order exists
    TEPS-REG-DISCOUNT: 10.00        // if order exists
    TEPS-REG-PACKAGES: 8x10,29.99,PKG1;Keychain,9.99,ADD1  // if packages

  Response 404: Not found.


================================================================================
3. WORKFLOW
================================================================================

  All workflow endpoints return the updated Registration object on success.


--- POST /api/registrations/{id}/checkin -------------------------------------

  Move from "registered" to "checked_in". Enters the host station queue.

  Precondition: status must be "registered".

  Side effects:
    - Status -> "checked_in"
    - current_station -> "host"
    - Queue log: "entered" at "host"

  Response 200: Updated Registration.
  Response 400: Wrong status.


--- POST /api/registrations/{id}/start ---------------------------------------

  Activate a waiting customer at their current station. The operator clicks
  "Start" to begin serving this customer.

  Precondition: must have a current_station.

  Side effects:
    - Queue log: "started" at current station
    - No status change (status reflects which station, not active/waiting)

  Response 200: Updated Registration.
  Response 400: Not at a station.

  Note: The "is_queue_active" enriched field will become true for this
  registration in subsequent GET /api/queue/{station} responses.


--- POST /api/registrations/{id}/advance -------------------------------------

  Move to the next station in the workflow, or mark complete if at the
  last station.

  Optional request body (fields to update during advance):
  {
    "elf_name": "Jingle",
    "child_wishlist": "Bicycle;Doll",
    "notes": "wishlist: Bicycle\nelf: Jingle"
  }

  Side effects:
    - Queue log: "exited" at current station
    - If next station exists:
        Status -> station-appropriate value
        current_station -> next station
        Queue log: "entered" at next station
    - If no next station (checkout is last):
        Status -> "complete"
        current_station -> null

  Station -> status mapping:
    host     -> "checked_in"
    elf      -> "elf_processed"
    camera   -> "photographed"
    preview  -> "previewing"
    checkout -> "checkout"
    (after checkout -> "complete")

  Response 200: Updated Registration + TEPS-REG-* headers.
  Response 400: Not at a station.


--- POST /api/registrations/{id}/return --------------------------------------

  Send back to the previous station.

  Precondition: must have a previous station (can't return from host).

  Side effects:
    - Queue log: "exited" at current station
    - Status -> previous station's status
    - current_station -> previous station
    - Queue log: "entered" at previous station

  Response 200: Updated Registration.
  Response 400: No previous station.


--- POST /api/registrations/{id}/cancel --------------------------------------

  Cancel a registration at any stage.

  Side effects:
    - Status -> "cancelled"
    - current_station -> null

  Response 200: Updated Registration.


================================================================================
4. QUEUE & OVERVIEW
================================================================================


--- GET /api/queue/{station_type} --------------------------------------------

  Get the queue for a specific station. Returns registrations currently at
  that station, ordered by active first, then by arrival time.

  station_type: host, elf, camera, preview, checkout

  Response 200: Array of Registration objects.
  Each includes "is_queue_active": true/false indicating whether the operator
  has started this customer.


--- GET /api/queue/{station_type}/current ------------------------------------

  Get the currently active (started) customer at a station. Used by the
  Santa display to show who's being photographed.

  For station_type "santa", checks both "camera" and "host" stations.

  Response 200: Registration object + TEPS-REG-* headers.
  Response 200 (no active customer): {"message": "No active customer"}


--- GET /api/overview --------------------------------------------------------

  Aggregate station statistics for the overview dashboard.

  Response 200:
  {
    "event_name": "Holiday Photos 2026",
    "total_registered": 45,
    "total_checked_in": 12,
    "total_complete": 28,
    "total_waiting": 8,
    "total_in_progress": 5,
    "stations": [
      {"type": "host", "name": "Host Station", "count": 3},
      {"type": "camera", "name": "Camera Station", "count": 2},
      {"type": "preview", "name": "Preview Station", "count": 1},
      {"type": "checkout", "name": "Checkout Station", "count": 2}
    ]
  }


--- GET /api/registrations/{id}/queuelog -------------------------------------

  Get the full queue log history for a registration.

  Response 200: Array of QueueLogEntry objects.
  [
    {
      "id": 1,
      "registration_id": 5,
      "sync_id": "uuid",
      "station_type": "host",
      "action": "entered",
      "actor_station_id": null,
      "timestamp": "2026-04-11T09:30:00Z"
    }
  ]


================================================================================
5. ORDERS & PACKAGES
================================================================================


--- POST /api/registrations/{id}/order ---------------------------------------

  Create an order for a registration. If one already exists, returns the
  existing order (idempotent).

  Request body (optional):
  {
    "promo_code": "HOLIDAY20",
    "notes": "Gift wrap requested",
    "fastpass": false
  }

  Response 201: Order object.
  Response 200: Existing Order (if already created).


--- GET /api/registrations/{id}/order ----------------------------------------

  Get the order for a registration.

  Response 200: Order object.
  {
    "id": 1,
    "registration_id": 5,
    "order_status": 0,
    "santa_slap": 0,
    "subtotal": 0,
    "tax": 0,
    "total": 0,
    "discount": 0,
    "promo_code": "HOLIDAY20",
    "notes": "Gift wrap requested",
    "order_text": null,
    "custom_fields": null,
    "fastpass": false,
    "fastpass_status": null,
    "last_action": null,
    "quantity": 1,
    "created_at": "2026-04-11T10:00:00Z",
    "updated_at": "2026-04-11T10:00:00Z",
    "packages": null
  }

  Response 404: No order found.


--- POST /api/orders/{id}/status ---------------------------------------------

  Update order status (and optionally santa_slap count).

  Request body:
  {
    "status": 3,              // 0=new, 1=elf, 2=photo, 3=POS, 9=cancelled
    "santa_slap": 2           // optional
  }

  Response 200: {"success": true}


--- POST /api/orders/{id}/packages -------------------------------------------

  Add a package line item to an order.

  Request body:
  {
    "title": "8x10 Print",    // required
    "code": "PKG-8X10",       // optional
    "price": 29.99,            // optional, default 0
    "is_addon": false          // optional, default false
  }

  Response 201: OrderPackage object.
  {
    "id": 1,
    "order_id": 1,
    "title": "8x10 Print",
    "code": "PKG-8X10",
    "price": 29.99,
    "is_addon": false
  }


--- GET /api/orders/{id}/packages --------------------------------------------

  List all packages on an order.

  Response 200: Array of OrderPackage objects.


================================================================================
6. UPLOADS & CODE CHANGE
================================================================================


--- POST /api/registrations/{id}/userdata ------------------------------------

  Upload customer data to the cloud (phototouchinc.com). Marks the
  registration's userdata_uploaded flag on success.

  Response 200:
  {
    "success": true,
    "response": "<server response body>"
  }

  Response 500:
  {
    "success": false,
    "error": "Upload failed: <reason>"
  }


--- POST /api/code/reassign -------------------------------------------------

  Reassign a photo code (rename the sitting folder).

  Request body:
  {
    "registration_id": 5,
    "new_code": "NEWCODE123",
    "filenames": ["IMG001.jpg", "IMG002.jpg"]  // optional, auto-detected
  }

  Checks that new_code is not already in use (409 if so).
  Updates the code in the database after renaming files.

  Response 200: ReassignResult object.
  Response 409: Code already in use.


--- POST /api/code/swap ------------------------------------------------------

  Swap codes between two registrations (swap their sitting folders).

  Request body:
  {
    "code_a": "CODE111",
    "code_b": "CODE222"
  }

  Response 200: SwapResult object.


--- GET /api/code/files/{code} -----------------------------------------------

  List files in the sitting folder for a photo code.

  Response 200: Array of file info objects.


================================================================================
7. THEME
================================================================================


--- GET /api/theme/css -------------------------------------------------------

  Returns CSS variables for the active theme.

  Response 200 (text/css):
    :root {
      --theme-bg: #1a1a2e;
      --theme-text: #ffffff;
      --theme-heading: #ffffff;
      --theme-accent: #e94560;
      ...
    }


--- GET /api/theme/background ------------------------------------------------

  Returns the custom background image if one is set.

  Response 200: Image data (image/png, image/jpeg, or image/webp).
  Response 404: No custom background set.


--- GET /api/theme/logo ------------------------------------------------------

  Returns the custom logo image if one is set.

  Response 200: Image data (image/png, image/jpeg, image/svg+xml, or image/webp).
  Response 404: No custom logo set.


================================================================================
8. TESTING
================================================================================


--- POST /api/test -----------------------------------------------------------

  Create a test registration for development/debugging.
  Code is "__TEST__", name is "Test Registration".

  Response 201: Registration object.


--- POST /api/test/cleanup ---------------------------------------------------

  Delete all test registrations (code = "__TEST__").

  Response 200:
  {
    "deleted": 3
  }


================================================================================
9. TEPS-REG RESPONSE HEADERS
================================================================================

  Certain endpoints include custom response headers for inter-station data
  transfer. Camera software and other station tools read these headers to
  get operational info without parsing JSON.

  SECURITY (TEPSTX-291): These headers no longer carry customer PII. The
  fields removed below leaked personal data into inter-station HTTP responses.
  The COMPLETE registration (including name, email, child name, etc.) is still
  returned in the JSON response body, so any client that needs personal data
  MUST read the body. Only non-PII operational fields remain in the headers.

  Header                    Value
  ----------------------    -----------------------------------------
  TEPS-REG-CODE             Photo code (non-PII correlation key)
  TEPS-REG-FASTPASS         "TRUE" or "FALSE"
  TEPS-REG-FASTPASSSTATUS   Fastpass status string
  TEPS-REG-TOTAL            Order total (e.g. "49.99")
  TEPS-REG-DISCOUNT         Order discount (e.g. "10.00")
  TEPS-REG-PACKAGES         Semicolon-separated: "Title,Price,Code;..."

  REMOVED (read from the JSON body instead):
    TEPS-REG-NAME, TEPS-REG-LASTNAME, TEPS-REG-EMAIL, TEPS-REG-CHILDNAME,
    TEPS-REG-PROMOCODE, TEPS-REG-ORDERTEXT

  Included on:
    - GET /api/registrations/code/{code}
    - POST /api/registrations/{id}/advance
    - GET /api/queue/{station_type}/current


================================================================================
END OF DOCUMENT
================================================================================
